<?php

namespace app\http\middleware;

use app\common\exception\BizException;
use Closure;
use think\Exception;
use think\db\exception\ModelNotFoundException;
use think\exception\ErrorException;
use think\exception\RouteNotFoundException;
use think\exception\ValidateException;
use think\facade\Log;
use think\helper\Arr;
use think\Response;
use think\response\Json;

/**
 * 发射器
 * 作为管理者，它不希望控制器的响应直接与请求者face to face，因此此处需要接管，将控制器方法的执行结果去安全的响应请求
 * 实际上发射器主要的目的是要将结果分发出去，异常部分处理没有问题，但是预期部分却因为不同的场景会有不同需求
 * 总之发射器需要满足各类需求场景，稳妥的将结果输出给请求者
 * Class Launcher
 * @package app\http\middleware
 */
class Launcher
{
    /**
     * api返回数据格式化中间件
     * @param $request
     * @param Closure $next
     * @return Response
     */
    public function handle($request, Closure $next):Response
    {
        /**
         * 关于中间件中try catch的问题解析
         * 所有的中间件中都有这段代码 $next($request)，可以把每个中间件理解成一截管道，所有的中间件有序的（按照执行顺序优先级）组装在一起就成了通道，
         * 假设代码运行过程中没异常的话，线程它是像水一样从这段管道中通过的，而前置中间件就是在程序进入该截管道前执行前面的逻辑，后置同理；
         * 如果在中间件中抛出异常，将直接会被上级的中间件容器捕获，后续管道以及控制器的代码都无法执行；
         * 如果在后置中间件中采用这种（如下）方法，要注意几点逻辑：
         * 1、首先这种写法只能捕获到目标controller（类）中的方法的异常，但是其他中间件的异常是无法捕获的，
         * 比如你有多个中间件串联，即便先前执行的中间件有异常抛出处理，此处也无法捕获（就是上面讲到的）。
         * 2、这种写法会截断排序在此中间件后面的所有后置中间件（即后面的中间件无法执行）。
         * 因此，在使用中间件并且有异常抛出处理的时候，一定要注意中间件执行顺序的问题。
         */
        $response = $next($request);
//        try{
//            $response = $next($request);
////            var_dump($response->getHeader());
//            $ret = self::format($response->getData());
//        }
//        //以下catch都是控制器中抛出的错误，无法捕获其他中间件抛出的异常，再强调一下，中间件中抛出的异常将直接被上级（应该是容器类）捕获
////        捕获路由丢失异常
//        catch (RouteNotFoundException $e) {
//            //这个是线上环境，只返回状态码
//            Log::record('[ 代码错误位置 ] '.$e->getFile().':'.$e->getLine(),'Exception');
//            Log::record('[ 未找到路由 ] '.$request->baseUrl(),'Exception');
//            $response = json()->code(404);// json type of Response class
//            $ret = 'Not Found';
//            //这个是测试环境，返回具体的错误信息
//            $response = json();
//            $ret = self::format(null,ROUTE_MISS,$e->getMessage());
//        }
////        捕获验证器异常
//        catch (ValidateException $e) {
//            //这里参数验证抛出的错误不知道该不该算是客户端错误，还是说算作是可预期错误 :-(
//            Log::record('[ 代码错误位置 ] '.$e->getFile().':'.$e->getLine(),'Exception');
//            Log::record('[ 验证类错误 ] '.$e->getMessage(),'Exception');
//            $response = json()->code(403);// json type of Response class
//            $ret = 'Forbidden';
//            //算了，还是当做可预期错误处理吧
//            $response = json();
//            $ret = self::format(null,ERRORS_PARAM,$e->getMessage());
//        }
////        业务逻辑类运行异常
//        catch (BizException $e) {
//            //此处抛出的错误为预期错误，因此允许正确响应请求
//            $response = json();
//            $ret = self::format(null,$e->getCode(),$e->getMessage());
//        }
//        catch (ModelNotFoundException $e) {
//            //此处抛出的错误为预期错误，因此允许正确响应请求
//            $response = json();
//            $ret = self::format(null,NULL_DATA);
//        }
////        有其他类型的错误可以继续添加
////            ...
//        catch (Exception $e) {
////            运行异常
//            Log::record('[ 代码错误位置 ] '.$e->getFile().':'.$e->getLine(),'Exception');
//            Log::record('[ 服务器错误 ] '.$e->getMessage(),'Exception');
//            $response = json()->code(500);// json type of Response class
//            $ret = 'Internal Server Error?';
//            $response = json();
//            $ret = self::format(null,ERRORS,$e->getMessage());
//        }
        //这个finally中的代码会在try(catch) return(如果有的话)之前执行，并且如果finally中含有return 那么try(catch)中的return就不会执行，具体逻辑搜索文档或者自己实践
//        finally {
//            if(isset($response))
//                $response->data($ret);
//        }
        /**
         * 稍微解释一下这个地方的逻辑处理，首先，作为发射器的本质是将响应安全的输出至请求者
         * 一般响应类型却有很多种，因为我们这里有一层接管，所以规范上一般开发中不用控制器自己去格式化封装响应结果
         * 但却需要明确输出类型，这是TP框架的软性要求与建议（阅读链接：https://www.kancloud.cn/manual/thinkphp6_0/1037526）
         * 因此我们就可以根据响应类型来做结果的最终封装
         * 又因为平常需求中返回的最多的就是数组，而数组都是json响应类型的，因此目前就只针对json响应做了封装处理
         * 后续可以根据业务需求封装其他类型响应输出
         */
        $data = $response->getData();
        /**
         * 这里的if逻辑又得详细介绍下
         * expr1：本来理想的json响应中getData()预期是array的，但是为了满足程序员的自由编码，所以允许控制器中return json('this is string')
         *        这么写，并且只有在数组的情况下才封装；规范上return json('this is string')是要写成return response('this is string')的
         * Ps: expr1这种因素不被考虑了，编码层面要严格格式要求
         * expr2：这个判断就比较深了，涉及到框架的请求流程问题，例如程序中抛出异常的时候，会有指定的ExceptionHandler来接管处理
         *        TP框架中就是由app\ExceptionHandle统一接管的；但是呢，理论上接管之后是应该将处理结果直接返回至请求者的
         *        但是在TP中，如果在exec控制器方法时抛出异常，TP会将异常处理结果再传递至中间件管道，因此该请求中的后置中间件还会预期执行，感觉有点隐患
         *        因为一些特殊需求，app\ExceptionHandle会将一些业务逻辑类异常视为预期结果格式化封装，等app\ExceptionHandle处理完之后
         *        单从发射器来看，他是无法识别此处的响应是来自控制器还是来自app\ExceptionHandle，为了避免此处重复封装
         *        所以提前判断一下该响应数据是否已经封装过
         */
        if($response instanceof Json) {
            if(
//                Arr::accessible($data)//expr1
//                &&
            !isset($data['errcode'])//expr2
            ) {
                $response->data(format($data));
            }
        }elseif($response instanceof Response) {
            $response->data((string)$data);
        }
        return $response;
    }

    /**
     * 接口返回值格式化处理
     * @param null $data
     * @param int $code
     * @param string $msg
     * @return mixed
     */
//    public static function format($data=null,$code=SUCCESS,$msg='')
//    {
//        $return['code'] = $code;
//        $return['data'] = ['msg' => $msg?:lang($code)];
//
//        // 根据不同数据类型来封装不同的返回值格式
//        switch (gettype($data)) {
//            case 'object':
//            case 'array':
//                $return['data'] = array_merge($return['data'],(array) $data);
//                break;
//            case 'NULL':
//            case 'boolean':
//                break;
//            case 'resource':
//                break;
//            case 'integer':
//            case 'double'://（如果是 float 则返回“double”，而不是“float”；详细参考gettype函数）
//            case 'string':
//            default:
//                $return = $data;
//                break;
//        }
//
//        return $return;
//    }
}